package digitRecognitionProblem.findMlpArgs;

import genetic_algorithm.Chromosome;
import genetic_algorithm.FitnessFunction;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.Map.Entry;

import mlp.Mlp;
import mlp.MlpExamples;
import mlp.UniformTrainExamples;
import utils.RandomGenerator;

public class MlpArgsFitness implements FitnessFunction {

	private int trainSize; // number of train examples
	private UniformTrainExamples trainExamples; // examples used to train networks generated out of chromosomes
	private Entry<ArrayList<float[]>,ArrayList<float[]>> trainData;
	private MlpExamples testExamples; // examples used to measure networks fitness
	private int epochs; // number of iterations of back propagations while training a new network
	private int numTries; // how many times checks fitness of a chromosome, after initializing its weights randomly	
	
	/**
	 * Constructor- creates a new object to evaluate
	 * chromosome's fitness
	 * @param trainSize number of train examples
	 * @param testExamples examples used to measure networks fitness
	 * @param epochs for how many iterations trains offsprings
	 * @param numTries how many times checks fitness of a chromosome, after initializing its weights randomly
	 */	
	public MlpArgsFitness(int trainSize, MlpExamples testExamples, int epochs,
			int numTries) {
		super();
		this.trainSize = trainSize;
		this.trainExamples = null;
		this.testExamples = testExamples;
		this.epochs = epochs;
		this.numTries = numTries;
		
		this.trainExamples = new UniformTrainExamples(MlpArgsMain.TRAIN_FILE, MlpArgsMain.DIGIT);
	}

	/**
	 * Calculates chromosome's fitness according to its
	 * neural network mean squared error 
	 */
	@Override
	public double getFitness(Chromosome chromosome) {
		
		// generate neural network out of chromosome's data				
		Mlp mlp = decode(chromosome);
		
		// train network
		mlp.learn(trainData.getKey(), trainData.getValue(), epochs);
		
		// test its fitness
		float mse = mlp.evaluateQuadraticError(testExamples.getInput(),
				testExamples.getOutput())
				/ ((float) testExamples.getInput().size());
		float bestFitness = 1.0f / (mse + 1.0f);
		
		// store network's arguments
		int[] structure = mlp.getStructure();
				
		// keep initializing random weights and return the best fitness
		float currFitness = 0.0f;
		for (int i = 0 ; i < numTries ; ++i) {
			
			// create a new network
			mlp = Mlp.createsByLayersSize(structure);
			
			// perform partial training
			mlp.learn(trainData.getKey(), trainData.getValue(), epochs);
			
			// test its fitness
			mse = mlp.evaluateQuadraticError(testExamples.getInput(),
					testExamples.getOutput())
					/ ((float) testExamples.getInput().size());
			currFitness = 1/(mse + 1);
			
			// update best fitness
			bestFitness = (currFitness < bestFitness) ? currFitness : bestFitness;
		}
		
		return bestFitness;
	}

	/**
	 * Builds a neural network according to data stored
	 * in given chromosome
	 * @param chromosome chromosome to be decoded
	 * @return neural network decoded from chromosome's data
	 */
	private Mlp decode(Chromosome chromosome) {
		
		// get binary data
		MlpArgsChromosome mlpArgs = (MlpArgsChromosome) chromosome;
		BitSet bits = mlpArgs.getData();
				
		// parse range for initial weights		
		int rangeIndex = 0;
		for (int i = MlpArgsChromosome.WEIGHTS_END - 1 ; i >= MlpArgsChromosome.WEIGHTS_START ; --i) { // read backwards
			rangeIndex += bits.get(i) ? (1 << (MlpArgsChromosome.WEIGHTS_END - 1 - i)) : 0;
		}
		float rangeLimit = MlpArgsChromosome.weightsRange(rangeIndex);
		
		// parse train examples to be used
		int offset = RandomGenerator.nextInt(38000 / trainSize);
		setTrainExamples(offset);
		
		// parse number of neurons in hidden layer
		int numHidden = 0;
		for (int i = MlpArgsChromosome.HIDDEN_END - 1 ; i >=  MlpArgsChromosome.HIDDEN_START ; --i) { // read backwards
			numHidden += bits.get(i) ? (1 << (MlpArgsChromosome.HIDDEN_END - 1 - i)) : 0;
		}
		
		// create the neural network		
		Mlp.INIT_WEIGHT_RANGE = rangeLimit;		
		int[] topology = { numHidden, Mlp.OUTPUT_LAYYER };
		Mlp mlp = Mlp.createsByLayersSize(topology);
		mlp.setThreshold(MlpArgsMain.THRESHOLD);
		
		return mlp;
	}
	
	private void setTrainExamples(int offset) {		
		trainData = trainExamples.getExamples(offset, trainSize);
	}
}
